﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Xml.Linq;
using NUnit.Framework;
using Utility3D.Extensions;

namespace Utility3D
{
    [TestFixture]
    public class WorldModelTest
    {
        [Test]
        public void SupportsBasicDrawing()
        {
            var worldModel = new WorldModel();

            var drawingPlane = new Plane(new Point3D(-1, -1, 0), new Vector3D(2, 0, 0), new Vector3D(0, 2, 0));
            var drawingSurface = new DrawingSurface(worldModel, "test", drawingPlane, 100, 100);

            var start = worldModel.Collections.AddPoint(drawingSurface, new Point(25, 25));
            var end = worldModel.Collections.AddPoint(drawingSurface, new Point(75, 75));
            var line = worldModel.Collections.AddStroke<LineStroke>(start, end);

            Assert.AreEqual("(-0.5,-0.5,0)", line.Start.GetPoint3DOnSurface(drawingSurface).ToString());
            Assert.AreEqual("(0.5,0.5,0)", line.End.GetPoint3DOnSurface(drawingSurface).ToString());

            worldModel.DrawingSurfaceChains.Add(drawingSurface);

            var viewingPlane = new Plane(new Point3D(-2, -2, -1), new Vector3D(4, 0, 0), new Vector3D(0, 4, 0));
            var focalPoint = new Point3D(0, 0, -2);
            var camera = new Camera(viewingPlane, focalPoint);

            var viewPort = new Viewport(worldModel, camera, 100, 100);
            var methods = viewPort.Render();

            var drawingTest = new DrawingTest();
            foreach (var method in methods)
                method(drawingTest);

            var drawnLine = drawingTest.DrawnLines.Last();
            Assert.AreEqual("Draw line from 43.75,43.75 to 56.25,56.25", drawnLine);
        }

        [Test]
        public void TestIntersectingLineDrawing()
        {
            var worldModel = DefaultWorldModelGenerator.GetModel();

            var drawingSurface = worldModel.DrawingSurfaceChains.First(ds => ds.Name == "Front View");
            var p1 = worldModel.Collections.AddPoint(drawingSurface, new Point(10, 90));
            var p2 = worldModel.Collections.AddPoint(drawingSurface, new Point(20, 110));
            worldModel.Collections.AddStroke<LineStroke>(p1,  p2);

            drawingSurface = worldModel.DrawingSurfaceChains.First(ds => ds.Name == "Top View");
            var points = worldModel.GetIntersectionPointsOnSurface(drawingSurface);

            Assert.AreEqual(1, points.Count());
            var point = points.Single();
            Assert.Less(Math.Abs(15 - point.X), 0.1);
            Assert.AreEqual(0, point.Y);
        }

        [Test]
        public void TrackZigZagIntersection()
        {
            var worldModel = DefaultWorldModelGenerator.GetModel();
            var drawingSurface = worldModel.DrawingSurfaceChains.First(ds => ds.Name == "Front View");
            drawingSurface = drawingSurface.Glide(50);

            var p1 = worldModel.Collections.AddPoint(drawingSurface, new Point(10, 90));
            var p2 = worldModel.Collections.AddPoint(drawingSurface, new Point(20, 80));
            var p3 = worldModel.Collections.AddPoint(drawingSurface, new Point(10, 60));
            var p4 = worldModel.Collections.AddPoint(drawingSurface, new Point(40, 40));
            worldModel.Collections.AddStroke<LineStroke>(p1, p2);
            worldModel.Collections.AddStroke<LineStroke>(p2, p3);
            worldModel.Collections.AddStroke<LineStroke>(p3, p4);

            drawingSurface = worldModel.DrawingSurfaceChains.First(ds => ds.Name == "Top View");
            drawingSurface = drawingSurface.Glide(15);
            var points = worldModel.GetIntersectionPointsOnSurface(drawingSurface);
            Assert.AreEqual(2, points.Count());
            var point = points.Single(p => p.Y != 0);
            Assert.AreEqual(15, point.X);
            Assert.AreEqual(50, point.Y);

            drawingSurface = drawingSurface.Glide(15);
            points = worldModel.GetIntersectionPointsOnSurface(drawingSurface);
            Assert.AreEqual(2, points.Count());
            point = points.Single(p => p.Y != 0);
            Assert.AreEqual(15, point.X);
            Assert.AreEqual(50, point.Y);
        }
    }

    public class DrawingTest : IDrawing
    {
        private readonly List<string> _drawnLines = new List<string>();
        public IEnumerable<string> DrawnLines { get { return _drawnLines; } }

        public void DrawLine(Point start, Point end)
        {
            _drawnLines.Add("Draw line from " + start + " to " + end);
        }
    }

    public class WorldModel
    {
        public WorldModel()
        {
            _collectionManager = new ModelObjectCollectionManager(this);
            _drawingSurfaceChains.AddHandler = item => AssignId(item);
            _strokes.AddHandler = item => AddShape(item);
        }

        private void AddShape(Stroke stroke)
        {
            var shape = stroke.Shape ?? new Shape {WorldModel = this};

            stroke.Shape = shape;
            shape.Strokes.Add(stroke);
            _shapes.Add(shape);

            var listsToAddTo = shape
                .Strokes
                .OfType<LineStroke>()
                .SelectMany(s => new[] {s.Start, s.End})
                .Select(pointRef => _pointRefToShapeMap.ContainsKey(pointRef)
                        ? _pointRefToShapeMap[pointRef]
                        : (_pointRefToShapeMap[pointRef] = new List<Shape>()));

            foreach (var shapes in listsToAddTo) shapes.Add(shape);
        }

        private readonly ListDictionary<IPointRef, Stroke> _pointRefToStrokeMap = new ListDictionary<IPointRef, Stroke>();

        private readonly Dictionary<IPointRef, List<Shape>> _pointRefToShapeMap = new Dictionary<IPointRef, List<Shape>>();
        public IEnumerable<Shape> ShapeFromPointRef(IPointRef pointRef)
        {
            return !_pointRefToShapeMap.ContainsKey(pointRef)
                       ? (IEnumerable<Shape>) new Shape[] {}
                       : _pointRefToShapeMap[pointRef];
        }

        private readonly List<Shape> _shapes = new List<Shape>();
        public IEnumerable<Shape> Shapes { get { return _shapes; } }

        private readonly NotifiableCollection<DrawingSurface> _drawingSurfaceChains = new NotifiableCollection<DrawingSurface>();
        public ICollection<DrawingSurface> DrawingSurfaceChains { get { return _drawingSurfaceChains;  } }

        public void AttachSurface(DrawingSurface drawingSurface)
        {
            AssignId(drawingSurface);
        }

        public IEnumerable<Point> GetIntersectionPointsOnSurface(DrawingSurface drawingSurface)
        {
            var surfacesToCompare = DrawingSurfaceChains
                .SelectMany(ds => ds.ChainWithSelf(d => d.NextSurface))
                .Where(ds => ds != drawingSurface)
                .ToList();


            var points = surfacesToCompare
                .SelectMany(ds => _strokes.OfType<LineStroke>()
                                      .Select(ls => new { StartPoint = ls.Start.GetPoint3DOnSurface(ds), EndPoint = ls.End.GetPoint3DOnSurface(ds)})
                                      .Where(a => a.StartPoint != null && a.EndPoint != null)
                                      .Select(a => drawingSurface.Plane.GetIntersection(a.StartPoint.Value, a.EndPoint.Value - a.StartPoint.Value, true))
                                      .Where(p => p != null)
                                      .Select(p => drawingSurface.Plane.ProjectTo2D(p.Value)))
                .ToList();

            return points;
        }

        private readonly List<IPointRef> _points = new List<IPointRef>();
        public IEnumerable<IPointRef> Points { get { return _points; } }

        private readonly NotifiableCollection<Stroke> _strokes = new NotifiableCollection<Stroke>();
        public IEnumerable<Stroke> Strokes { get { return _strokes; } }

        private readonly ModelObjectCollectionManager _collectionManager;
        public ModelObjectCollectionManager Collections { get { return _collectionManager; } }

        public void AddObject(object o)
        {
            var modelObject = (IModelObject) o;
            if (typeof(DrawingSurface).IsAssignableFrom(o.GetType()))
                _drawingSurfaceChains.Add((DrawingSurface) o);
            else if (typeof(IPointRef).IsAssignableFrom(o.GetType()))
                _points.Add((IPointRef) o);
            else if (typeof(Stroke).IsAssignableFrom(o.GetType()))
                _strokes.Add((Stroke) o);
            else throw new Exception("Attempting to add unknown object type");

            _currentId = modelObject.Id + 1;
        }

        public XDocument ToXml()
        {
            var worldModelElement = new XElement("world_model");
            var document = new XDocument(worldModelElement);

            var elements = _drawingSurfaceChains.SelectMany(drawingSurface => drawingSurface.ChainWithSelf(d => d.NextSurface))
                .Concat<IModelObject>(_points)
                .Concat(_strokes)
                .Select(o => ModelObjectXmlSerializer.GetXml(o, o.GetType().Name));

            worldModelElement.Add(elements);

            return document;
        }

        private int _currentId = 1;

        private void AssignId(IModelObject modelObject)
        {
            if (modelObject.Id != 0) return;

            var property = modelObject
                .GetType()
                .GetProperty("Id", BindingFlags.Public | BindingFlags.Instance)
                .DeclaringType
                .GetProperty("Id", BindingFlags.Public | BindingFlags.Instance);

            property.SetValue(modelObject, _currentId, null);
            _currentId++;
        }

        public void ReplacePointRefWithNewPointRef(IPointRef oldPointRef, IPointRef newPointRef)
        {
            var oldStrokes = _pointRefToStrokeMap[oldPointRef];
            foreach (var stroke in oldStrokes)
            {
                if (stroke.Start == oldPointRef)
                    stroke.Start = newPointRef;
                if (stroke.End == oldPointRef)
                    stroke.End = newPointRef;
            }
            _pointRefToStrokeMap[newPointRef].AddRange(_pointRefToStrokeMap[oldPointRef]);
            _pointRefToStrokeMap.Remove(oldPointRef);
        }

        public class ModelObjectCollectionManager
        {
            private readonly WorldModel _worldModel;

            internal ModelObjectCollectionManager(WorldModel worldModel)
            {
                _worldModel = worldModel;
            }

            public IPointRef AddPoint<T>(DrawingSurface drawingSurface, Point point)
                where T : IPointRef
            {
                var pointRef = (IPointRef) Activator.CreateInstance(typeof (T));

                pointRef.SetOnSurface(drawingSurface, point);
                _worldModel.AssignId(pointRef);
                _worldModel._points.Add(pointRef);
                return pointRef;
            }

            public IPointRef AddPoint(DrawingSurface drawingSurface, Point point)
            {
//                var pointRef = new MultiSurfacePointRef();
                var pointRef = new SingleSurfacePointRef();

                if (drawingSurface.Transient)
                    drawingSurface.WorldModel.AttachSurface(drawingSurface);
                pointRef.SetOnSurface(drawingSurface, point);
                _worldModel.AssignId(pointRef);
                _worldModel._points.Add(pointRef);
                return pointRef;
            }

            public TStroke AddStroke<TStroke>(IPointRef start, IPointRef end, params Action<TStroke>[] actions)
                where TStroke : Stroke
            {
                var stroke = (TStroke)Activator.CreateInstance(typeof (TStroke));
                _worldModel.AssignId(stroke);
                foreach (var action in actions) action(stroke);
                stroke.Start = start;
                stroke.End = end;
                _worldModel._pointRefToStrokeMap[start].Add(stroke);
                _worldModel._pointRefToStrokeMap[end].Add(stroke);
                _worldModel._strokes.Add(stroke);

                var lineStroke = stroke as LineStroke;
                if (lineStroke != null)
                {
                    var iPointRef = IntersectedPointRef.FromLineStroke(lineStroke);
                    _worldModel._points.Add(iPointRef);
                }
                return stroke;
            }
        }
    }
}